﻿
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Workflow.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;
using System.Diagnostics;
using TextFlow.Activities;

namespace TextFlow.Engine
{
    internal static class DynamicActivityGenerator
    {
        private static Dictionary<AssemblyName, Type> _outputTypes = new Dictionary<AssemblyName, Type>( new AssemblyNameComparer() );
        private static Dictionary<AssemblyName, Type> _inputTypes = new Dictionary<AssemblyName, Type>( new AssemblyNameComparer() );

        public static ActionOutputActivity CreateOutputActivity( string boundPropertyName, Type boundPropertyType, string activityName )
        {
            if ( string.IsNullOrEmpty( boundPropertyName ) )
            {
                throw new ArgumentNullException( "boundPropertyName" );
            }

            if ( boundPropertyType == null )
            {
                throw new ArgumentNullException( "boundPropertyType" );
            }

            if ( string.IsNullOrEmpty( activityName ) )
            {
                throw new ArgumentNullException( "activityName" );
            }

            AssemblyName name = GetAssemblyName( boundPropertyName, boundPropertyType );

            Type activityType = null;

            lock ( _outputTypes )
            {
                if ( _outputTypes.ContainsKey( name ) )
                {
                    activityType = _outputTypes[ name ];
                }
                else
                {
                    AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly( name, AssemblyBuilderAccess.Run );

                    ModuleBuilder mb = ab.DefineDynamicModule( name.Name );

                    TypeBuilder tb = mb.DefineType( "ActionOutputActivity__", TypeAttributes.Public, typeof( ActionOutputActivity ) );

                    FieldBuilder fbDepProp = tb.DefineField( string.Format( "{0}Property", boundPropertyName ), typeof( DependencyProperty ), FieldAttributes.Public | FieldAttributes.Static );

                    ConstructorBuilder sctor = tb.DefineTypeInitializer();

                    ILGenerator sctorIL = sctor.GetILGenerator();
                    sctorIL.Emit( OpCodes.Ldstr, boundPropertyName );
                    sctorIL.Emit( OpCodes.Ldtoken, boundPropertyType );
                    sctorIL.Emit( OpCodes.Call, typeof( Type ).GetMethod( "GetTypeFromHandle" ) );
                    sctorIL.Emit( OpCodes.Ldtoken, tb );
                    sctorIL.Emit( OpCodes.Call, typeof( Type ).GetMethod( "GetTypeFromHandle" ) );
                    sctorIL.Emit( OpCodes.Call, typeof( DependencyProperty ).GetMethod( "Register", new Type[] { typeof( string ), typeof( Type ), typeof( Type ) } ) );
                    sctorIL.Emit( OpCodes.Stsfld, fbDepProp );
                    sctorIL.Emit( OpCodes.Ret );

                    ConstructorBuilder ctor1 = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof( string ) } );

                    ILGenerator ctor1IL = ctor1.GetILGenerator();
                    ctor1IL.Emit( OpCodes.Ldarg_0 );
                    ctor1IL.Emit( OpCodes.Ldarg_1 );
                    ctor1IL.Emit( OpCodes.Call, typeof( ActionOutputActivity ).GetConstructor( new Type[] { typeof( string ) } ) );
                    ctor1IL.Emit( OpCodes.Ret );

                    ConstructorBuilder ctor0 = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes );

                    ILGenerator ctor0IL = ctor0.GetILGenerator();
                    ctor0IL.Emit( OpCodes.Ldarg_0 );
                    ctor0IL.Emit( OpCodes.Call, typeof( ActionOutputActivity ).GetConstructor( Type.EmptyTypes ) );
                    ctor0IL.Emit( OpCodes.Ret );

                    PropertyBuilder pbVal = tb.DefineProperty( boundPropertyName, PropertyAttributes.HasDefault, boundPropertyType, null );

                    MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;

                    MethodBuilder mbValGetAccessor = tb.DefineMethod( string.Format( "get_{0}", boundPropertyName ), getSetAttr, boundPropertyType, null );

                    ILGenerator valGetIL = mbValGetAccessor.GetILGenerator();

                    LocalBuilder CS10000 = valGetIL.DeclareLocal( boundPropertyType );
                    Label label20 = valGetIL.DefineLabel();

                    valGetIL.Emit( OpCodes.Ldarg_0 );
                    valGetIL.Emit( OpCodes.Ldsfld, fbDepProp );
                    valGetIL.Emit( OpCodes.Call, typeof( DependencyObject ).GetMethod( "GetValue" ) );

                    if ( boundPropertyType.IsValueType )
                    {
                        valGetIL.Emit( OpCodes.Unbox_Any, boundPropertyType );
                    }
                    else
                    {
                        valGetIL.Emit( OpCodes.Castclass, boundPropertyType );
                    }

                    valGetIL.Emit( OpCodes.Stloc_0 );
                    valGetIL.Emit( OpCodes.Br_S, label20 );
                    valGetIL.MarkLabel( label20 );
                    valGetIL.Emit( OpCodes.Ldloc_0 );
                    valGetIL.Emit( OpCodes.Ret );

                    MethodBuilder mbValSetAccessor = tb.DefineMethod( string.Format( "set_{0}", boundPropertyName ), getSetAttr, null, new Type[] { boundPropertyType } );

                    ILGenerator valSetIL = mbValSetAccessor.GetILGenerator();
                    valSetIL.Emit( OpCodes.Ldarg_0 );
                    valSetIL.Emit( OpCodes.Ldsfld, fbDepProp );
                    valSetIL.Emit( OpCodes.Ldarg_1 );

                    if ( boundPropertyType.IsValueType )
                    {
                        valSetIL.Emit( OpCodes.Box, boundPropertyType );
                    }

                    valSetIL.Emit( OpCodes.Call, typeof( DependencyObject ).GetMethod( "SetValue" ) );
                    valSetIL.Emit( OpCodes.Ret );

                    pbVal.SetGetMethod( mbValGetAccessor );
                    pbVal.SetSetMethod( mbValSetAccessor );

                    activityType = tb.CreateType();

                    _outputTypes.Add( name, activityType );
                }
            }

            Debug.Assert( activityType != null );

            object result = Activator.CreateInstance( activityType, activityName );

            return result as ActionOutputActivity;
        }

        public static ActionInputActivity CreateInputActivity( string boundPropertyName, Type boundPropertyType, string activityName )
        {
            if ( string.IsNullOrEmpty( boundPropertyName ) )
            {
                throw new ArgumentNullException( "boundPropertyName" );
            }

            if ( boundPropertyType == null )
            {
                throw new ArgumentNullException( "boundPropertyType" );
            }

            if ( string.IsNullOrEmpty( activityName ) )
            {
                throw new ArgumentNullException( "activityName" );
            }

            AssemblyName name = GetAssemblyName( boundPropertyName, boundPropertyType );

            Type activityType = null;

            lock ( _inputTypes )
            {
                if ( _inputTypes.ContainsKey( name ) )
                {
                    activityType = _inputTypes[ name ];
                }
                else
                {
                    AssemblyBuilder ab = AppDomain.CurrentDomain.DefineDynamicAssembly( name, AssemblyBuilderAccess.Run );

                    ModuleBuilder mb = ab.DefineDynamicModule( name.Name );

                    TypeBuilder tb = mb.DefineType( "ActionInputActivity__", TypeAttributes.Public, typeof( ActionInputActivity ) );

                    FieldBuilder fbBackingStore = tb.DefineField( string.Format( "_{0}", boundPropertyName ), boundPropertyType, FieldAttributes.Public );

                    ConstructorBuilder ctor1 = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof( string ) } );

                    ILGenerator ctor1IL = ctor1.GetILGenerator();
                    ctor1IL.Emit( OpCodes.Ldarg_0 );
                    ctor1IL.Emit( OpCodes.Ldarg_1 );
                    ctor1IL.Emit( OpCodes.Call, typeof( ActionOutputActivity ).GetConstructor( new Type[] { typeof( string ) } ) );
                    ctor1IL.Emit( OpCodes.Ret );

                    ConstructorBuilder ctor0 = tb.DefineConstructor( MethodAttributes.Public, CallingConventions.Standard, Type.EmptyTypes );

                    ILGenerator ctor0IL = ctor0.GetILGenerator();
                    ctor0IL.Emit( OpCodes.Ldarg_0 );
                    ctor0IL.Emit( OpCodes.Call, typeof( ActionOutputActivity ).GetConstructor( Type.EmptyTypes ) );
                    ctor0IL.Emit( OpCodes.Ret );

                    PropertyBuilder pbVal = tb.DefineProperty( boundPropertyName, PropertyAttributes.HasDefault, boundPropertyType, null );

                    MethodAttributes getSetAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;

                    MethodBuilder mbValGetAccessor = tb.DefineMethod( string.Format( "get_{0}", boundPropertyName ), getSetAttr, boundPropertyType, null );

                    ILGenerator valGetIL = mbValGetAccessor.GetILGenerator();

                    LocalBuilder CS10000 = valGetIL.DeclareLocal( boundPropertyType );
                    Label label20 = valGetIL.DefineLabel();

                    valGetIL.Emit( OpCodes.Ldarg_0 );
                    valGetIL.Emit( OpCodes.Ldfld, fbBackingStore );
                    valGetIL.Emit( OpCodes.Stloc_0 );
                    valGetIL.Emit( OpCodes.Br_S, label20 );
                    valGetIL.MarkLabel( label20 );
                    valGetIL.Emit( OpCodes.Ldloc_0 );
                    valGetIL.Emit( OpCodes.Ret );

                    MethodBuilder mbValSetAccessor = tb.DefineMethod( string.Format( "set_{0}", boundPropertyName ), getSetAttr, null, new Type[] { boundPropertyType } );

                    ILGenerator valSetIL = mbValSetAccessor.GetILGenerator();
                    valSetIL.Emit( OpCodes.Ldarg_0 );
                    valSetIL.Emit( OpCodes.Ldarg_1 );
                    valSetIL.Emit( OpCodes.Stfld, fbBackingStore );
                    valSetIL.Emit( OpCodes.Ret );

                    pbVal.SetGetMethod( mbValGetAccessor );
                    pbVal.SetSetMethod( mbValSetAccessor );

                    activityType = tb.CreateType();

                    _inputTypes.Add( name, activityType );
                }
            }

            Debug.Assert( activityType != null );

            object result = Activator.CreateInstance( activityType, activityName );

            return result as ActionInputActivity;
        }

        public static IEnumerable<Assembly> Assemblies
        {
            get
            {
                foreach ( Type t in _outputTypes.Values )
                {
                    yield return t.Assembly;
                }

                foreach ( Type t in _inputTypes.Values )
                {
                    yield return t.Assembly;
                }
            }
        }

        private static AssemblyName GetAssemblyName( string boundPropertyName, Type boundPropertyType )
        {
            Debug.Assert( string.IsNullOrEmpty( boundPropertyName ) == false );
            Debug.Assert( boundPropertyType != null );

            return new AssemblyName( string.Format( "{0} - {1}", boundPropertyName, boundPropertyType.FullName ) );
        }
    }

    internal class AssemblyNameComparer : IEqualityComparer<AssemblyName>
    {
        public bool Equals( AssemblyName x, AssemblyName y )
        {
            return x.Name == y.Name;
        }

        public int GetHashCode( AssemblyName obj )
        {
            return obj.Name.GetHashCode();
        }
    }
}
